/*
 * Copyright 2009 Mike Cumings
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kenai.jbosh;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Implementation of the BodyParser interface which uses the SAX API that is
 * part of the JDK. Due to the fact that we can cache and reuse SAXPArser
 * instances, this has proven to be significantly faster than the use of the
 * javax.xml.stream API introduced in Java 6 while simultaneously providing an
 * implementation accessible to Java 5 users.
 */
final class BodyParserSAX implements BodyParser {

	/**
	 * Logger.
	 */
	private static final Logger LOG = Logger.getLogger(BodyParserSAX.class
			.getName());

	/**
	 * SAX parser factory.
	 */
	private static final SAXParserFactory SAX_FACTORY;
	static {
		SAX_FACTORY = SAXParserFactory.newInstance();
		SAX_FACTORY.setNamespaceAware(true);
		SAX_FACTORY.setValidating(false);
	}

	/**
	 * Thread local to contain a SAX parser instance for each thread that
	 * attempts to use one. This allows us to gain an order of magnitude of
	 * performance as a result of not constructing parsers for each invocation
	 * while retaining thread safety.
	 */
	private static final ThreadLocal<SoftReference<SAXParser>> PARSER = new ThreadLocal<SoftReference<SAXParser>>() {
		@Override
		protected SoftReference<SAXParser> initialValue() {
			return new SoftReference<SAXParser>(null);
		}
	};

	/**
	 * SAX event handler class.
	 */
	private static final class Handler extends DefaultHandler {
		private final BodyParserResults result;
		private final SAXParser parser;
		private String defaultNS = null;

		private Handler(SAXParser theParser, BodyParserResults results) {
			parser = theParser;
			result = results;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void startElement(final String uri, final String localName,
				final String qName, final Attributes attributes) {
			if (LOG.isLoggable(Level.FINEST)) {
				LOG.finest("Start element: " + qName);
				LOG.finest("    URI: " + uri);
				LOG.finest("    local: " + localName);
			}

			BodyQName bodyName = AbstractBody.getBodyQName();
			// Make sure the first element is correct
			if (!(bodyName.getNamespaceURI().equals(uri) && bodyName
					.getLocalPart().equals(localName))) {
				throw (new IllegalStateException("Root element was not '"
						+ bodyName.getLocalPart() + "' in the '"
						+ bodyName.getNamespaceURI() + "' namespace.  (Was '"
						+ localName + "' in '" + uri + "')"));
			}

			// Read in the attributes, making sure to expand the namespaces
			// as needed.
			for (int idx = 0; idx < attributes.getLength(); idx++) {
				String attrURI = attributes.getURI(idx);
				if (attrURI.length() == 0) {
					attrURI = defaultNS;
				}
				String attrLN = attributes.getLocalName(idx);
				String attrVal = attributes.getValue(idx);
				if (LOG.isLoggable(Level.FINEST)) {
					LOG.finest("    Attribute: {" + attrURI + "}" + attrLN
							+ " = '" + attrVal + "'");
				}

				BodyQName aqn = BodyQName.create(attrURI, attrLN);
				result.addBodyAttributeValue(aqn, attrVal);
			}

			parser.reset();
		}

		/**
		 * {@inheritDoc}
		 * 
		 * This implementation uses this event hook to keep track of the default
		 * namespace on the body element.
		 */
		@Override
		public void startPrefixMapping(final String prefix, final String uri) {
			if (prefix.length() == 0) {
				if (LOG.isLoggable(Level.FINEST)) {
					LOG.finest("Prefix mapping: <DEFAULT> => " + uri);
				}
				defaultNS = uri;
			} else {
				if (LOG.isLoggable(Level.FINEST)) {
					LOG.info("Prefix mapping: " + prefix + " => " + uri);
				}
			}
		}
	}

	// /////////////////////////////////////////////////////////////////////////
	// BodyParser interface methods:

	/**
	 * {@inheritDoc}
	 */
	public BodyParserResults parse(String xml) throws BOSHException {
		BodyParserResults result = new BodyParserResults();
		Exception thrown;
		try {
			InputStream inStream = new ByteArrayInputStream(xml.getBytes());
			SAXParser parser = getSAXParser();
			parser.parse(inStream, new Handler(parser, result));
			return result;
		} catch (SAXException saxx) {
			thrown = saxx;
		} catch (IOException iox) {
			thrown = iox;
		}
		throw (new BOSHException("Could not parse body:\n" + xml, thrown));
	}

	// /////////////////////////////////////////////////////////////////////////
	// Private methods:

	/**
	 * Gets a SAXParser for use in parsing incoming messages.
	 * 
	 * @return parser instance
	 */
	private static SAXParser getSAXParser() {
		SoftReference<SAXParser> ref = PARSER.get();
		SAXParser result = ref.get();
		if (result == null) {
			Exception thrown;
			try {
				result = SAX_FACTORY.newSAXParser();
				ref = new SoftReference<SAXParser>(result);
				PARSER.set(ref);
				return result;
			} catch (ParserConfigurationException ex) {
				thrown = ex;
			} catch (SAXException ex) {
				thrown = ex;
			}
			throw (new IllegalStateException("Could not create SAX parser",
					thrown));
		} else {
			result.reset();
			return result;
		}
	}

}
